/* JAI-Ext - OpenSource Java Advanced Image Extensions Library
*    http://www.geo-solutions.it/
*    Copyright 2014 GeoSolutions


* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at

* http://www.apache.org/licenses/LICENSE-2.0

* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.imagen.media.affine;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.io.IOException;
import org.eclipse.imagen.BorderExtender;
import org.eclipse.imagen.ImageLayout;
import org.eclipse.imagen.Interpolation;
import org.eclipse.imagen.JAI;
import org.eclipse.imagen.PlanarImage;
import org.eclipse.imagen.ROI;
import org.eclipse.imagen.ROIShape;
import org.eclipse.imagen.RenderedOp;
import org.eclipse.imagen.media.range.Range;
import org.eclipse.imagen.media.range.RangeFactory;
import org.eclipse.imagen.media.testclasses.TestBase;
import org.eclipse.imagen.media.viewer.RenderedImageBrowser;

/**
 * This test-class is an extension of the TestBase class inside the jt-utilities project. By calling the
 * testGlobalAffine() method with the selected parameters is possible to create an image with the selected preferences
 * and then process it with the preferred interpolation type. Inside the testGlobalAffine() method are tested images
 * with all the possible data type by calling the testImageAffine() method. This method is used for creating an image
 * with the user-defined parameters(data type, ROI, No Data Range) and then transforming it with 4 possible
 * transformations:
 *
 * <ul>
 *   <li>Only Rotation
 *   <li>Only Scaling
 *   <li>Only Translation
 *   <li>Combination of the 3 above
 * </ul>
 *
 * The affine transformation is performed with the selected interpolation type. If the user wants to see the result
 * image with the selected kind of test, must set JAI.Ext.Interactive parameter to true, JAI.Ext.TestSelector from 0 to
 * 5, JAI.Ext.TransformationSelector from 0 to 3 (one of the above described transformations)and JAI.Ext.InverseScale to
 * 0 or 1 (Magnification/reduction) to the Console. The testAllOperation() method is used for grouping all the tests on
 * the same image with 4 different transformations in only one test-method. The methods testImage() and testGlobal() are
 * not supported, they are defined in the jt-scale project.
 */
public class TestAffine extends TestBase {

    /** Quadrant rotation number for the Affine transformation */
    protected int numquadrants = 1;

    /** X coordinate point for rotation */
    protected double anchorX = 0;

    /** Y coordinate point for rotation */
    protected double anchorY = DEFAULT_HEIGHT - 1;

    /** Integer indicating which operation should be visualized */
    public static Integer TRANSFORMATION_SELECTOR = Integer.getInteger("JAI.Ext.TransformationSelector");

    protected float transY = -DEFAULT_HEIGHT;

    protected <T extends Number & Comparable<? super T>> void testImageAffine(
            RenderedImage sourceImage,
            int dataType,
            T noDataValue,
            boolean useROIAccessor,
            boolean isBinary,
            boolean bicubic2Disabled,
            boolean noDataRangeUsed,
            boolean roiPresent,
            boolean setDestinationNoData,
            TransformationType transformType,
            InterpolationType interpType,
            TestSelection testSelect,
            ScaleType scaleValue) {

        if (scaleValue == ScaleType.REDUCTION) {
            scaleX = 0.5f;
            scaleY = 0.5f;
        } else {
            scaleX = 1.5f;
            scaleY = 1.5f;
        }

        // No Data Range
        Range noDataRange = null;

        if (noDataRangeUsed && !isBinary) {
            switch (dataType) {
                case DataBuffer.TYPE_BYTE:
                    noDataRange = RangeFactory.create(noDataValue.byteValue(), true, noDataValue.byteValue(), true);
                    break;
                case DataBuffer.TYPE_USHORT:
                    noDataRange = RangeFactory.create(noDataValue.shortValue(), true, noDataValue.shortValue(), true);
                    break;
                case DataBuffer.TYPE_SHORT:
                    noDataRange = RangeFactory.create(noDataValue.shortValue(), true, noDataValue.shortValue(), true);
                    break;
                case DataBuffer.TYPE_INT:
                    noDataRange = RangeFactory.create(noDataValue.intValue(), true, noDataValue.intValue(), true);
                    break;
                case DataBuffer.TYPE_FLOAT:
                    noDataRange =
                            RangeFactory.create(noDataValue.floatValue(), true, noDataValue.floatValue(), true, true);
                    break;
                case DataBuffer.TYPE_DOUBLE:
                    noDataRange =
                            RangeFactory.create(noDataValue.doubleValue(), true, noDataValue.doubleValue(), true, true);
                    break;
                default:
                    throw new IllegalArgumentException("Wrong data type");
            }
        }

        // ROI
        ROIShape roi = null;

        if (roiPresent) {
            roi = roiCreation();
        }

        // Hints are used only with roiAccessor
        RenderingHints hints = null;

        if (useROIAccessor) {
            hints = new RenderingHints(
                    JAI.KEY_BORDER_EXTENDER, BorderExtender.createInstance(BorderExtender.BORDER_ZERO));
        }

        // Affine Transform selection
        AffineTransform transform = null;
        if (transformType == TransformationType.ROTATE_OP) {
            // Rotation
            transform = AffineTransform.getQuadrantRotateInstance(numquadrants, anchorX, anchorY);
        } else if (transformType == TransformationType.SCALE_OP) {
            // Scale (X and Y doubled)
            transform = AffineTransform.getScaleInstance(scaleX, scaleY);
        } else if (transformType == TransformationType.TRANSLATE_OP) {
            transX = DEFAULT_WIDTH;
            transY = 0;
            // Translation
            transform = AffineTransform.getTranslateInstance(transX, transY);
        } else if (transformType == TransformationType.ALL) {
            transX = 0;
            transY = -DEFAULT_HEIGHT;
            // Rotation
            transform = AffineTransform.getQuadrantRotateInstance(numquadrants, anchorX, anchorY);
            // + Scale (X and Y doubled)
            transform.concatenate(AffineTransform.getScaleInstance(scaleX, scaleY));
            // + Translation (translation towards the center of the image)
            transform.concatenate(AffineTransform.getTranslateInstance(transX, transY));
        } else {
            transform = new AffineTransform();
        }

        RenderedImage destinationIMG = null;

        // Interpolator initialization
        Interpolation interpN = null;
        Interpolation interpB = null;
        Interpolation interpBN = null;

        // Interpolators
        switch (interpType) {
            case NEAREST_INTERP:
                // Nearest-Neighbor
                interpN = new org.eclipse.imagen.InterpolationNearest();

                // Affine operation
                destinationIMG = AffineDescriptor.create(
                        sourceImage,
                        transform,
                        interpN,
                        new double[] {destinationNoData},
                        (ROI) roi,
                        useROIAccessor,
                        setDestinationNoData,
                        noDataRange,
                        hints);

                break;
            case BILINEAR_INTERP:
                // Bilinear
                interpB = new org.eclipse.imagen.InterpolationBilinear(DEFAULT_SUBSAMPLE_BITS);

                if (hints != null) {
                    hints.add(new RenderingHints(
                            JAI.KEY_BORDER_EXTENDER, BorderExtender.createInstance(BorderExtender.BORDER_COPY)));
                } else {
                    hints = new RenderingHints(
                            JAI.KEY_BORDER_EXTENDER, BorderExtender.createInstance(BorderExtender.BORDER_COPY));
                }

                // Affine operation
                destinationIMG = AffineDescriptor.create(
                        sourceImage,
                        transform,
                        interpB,
                        new double[] {destinationNoData},
                        (ROI) roi,
                        useROIAccessor,
                        setDestinationNoData,
                        noDataRange,
                        hints);

                break;
            case BICUBIC_INTERP:
                // Bicubic
                interpBN = new org.eclipse.imagen.InterpolationBicubic(DEFAULT_SUBSAMPLE_BITS);

                if (hints != null) {
                    hints.add(new RenderingHints(
                            JAI.KEY_BORDER_EXTENDER, BorderExtender.createInstance(BorderExtender.BORDER_COPY)));
                } else {
                    hints = new RenderingHints(
                            JAI.KEY_BORDER_EXTENDER, BorderExtender.createInstance(BorderExtender.BORDER_COPY));
                }

                // Affine operation
                destinationIMG = AffineDescriptor.create(
                        sourceImage,
                        transform,
                        interpBN,
                        new double[] {destinationNoData},
                        (ROI) roi,
                        useROIAccessor,
                        setDestinationNoData,
                        noDataRange,
                        hints);

                break;
            default:
                break;
        }

        if (INTERACTIVE
                && dataType == DataBuffer.TYPE_BYTE
                && TEST_SELECTOR == testSelect.getType()
                && TRANSFORMATION_SELECTOR == transformType.getValue()
                && INVERSE_SCALE == scaleValue.getType()) {
            RenderedImageBrowser.showChain(destinationIMG, false, roiPresent);
            try {
                System.in.read();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            // Forcing to retrieve an array of all the image tiles
            ((PlanarImage) destinationIMG).getTiles();
        }
        // Control if the operation has been correctly performed(can be done only because
        // the image is a square and so even if it is rotated, its dimensions are unchanged).

        // Control if the ROI has been expanded
        PlanarImage planarIMG = (PlanarImage) destinationIMG;

        if (transformType == TransformationType.SCALE_OP) {
            if (!isBinary && roiPresent) {
                // Control if the ROI has been expanded
                int imgWidthROI = destinationIMG.getWidth() * 3 / 4 - 2;
                int imgHeightROI = destinationIMG.getHeight() * 3 / 4 - 2;

                int tileInROIx = planarIMG.XToTileX(imgWidthROI);
                int tileInROIy = planarIMG.YToTileY(imgHeightROI);

                Raster testTile = destinationIMG.getTile(tileInROIx, tileInROIy);

                boolean interpNear = false;
                if (interpN != null) {
                    interpNear = true;
                }

                testROI(dataType, testTile, interpNear);

                // Check minimum and maximum value for a tile
                int xFirstTile = destinationIMG.getMinTileX() + destinationIMG.getNumXTiles() - 1;
                int ySecondTile = destinationIMG.getMinTileY() + destinationIMG.getNumYTiles() - 1;

                Raster simpleTile = destinationIMG.getTile(xFirstTile, ySecondTile);

                testEmptyImage(dataType, simpleTile, isBinary, interpNear);
            }
            // width
            assertEquals((int) (DEFAULT_WIDTH * scaleX), destinationIMG.getWidth());
            // height
            assertEquals((int) (DEFAULT_HEIGHT * scaleY), destinationIMG.getHeight());
        } else if (transformType == TransformationType.TRANSLATE_OP) {
            if (!isBinary && roiPresent) {
                // Control if the ROI has been expanded
                int imgWidthROI = destinationIMG.getMinX() + destinationIMG.getWidth() / 4 - 1;
                int imgHeightROI = destinationIMG.getMinY() + destinationIMG.getHeight() * 3 / 4 - 1;

                int tileInROIx = planarIMG.XToTileX(imgWidthROI);
                int tileInROIy = planarIMG.YToTileY(imgHeightROI);

                Raster testTile = destinationIMG.getTile(tileInROIx, tileInROIy);

                boolean interpNear = false;
                if (interpN != null) {
                    interpNear = true;
                }

                testROI(dataType, testTile, interpNear);

                // Check minimum and maximum value for a tile
                int xFirstTile = destinationIMG.getMinTileX() + destinationIMG.getNumXTiles() - 1;
                int ySecondTile = destinationIMG.getMinTileY() + destinationIMG.getNumYTiles() - 1;

                Raster simpleTile = destinationIMG.getTile(xFirstTile, ySecondTile);
                testEmptyImage(dataType, simpleTile, isBinary, interpNear);
            }
            double actualX = destinationIMG.getMinX();
            double actualY = destinationIMG.getMinY();

            double expectedX = sourceImage.getMinX() + transX;
            double expectedY = sourceImage.getMinY() + transY;

            double tolerance = 0.1f;
            // X axis
            assertEquals(expectedX, actualX, tolerance);
            // Y axis
            assertEquals(expectedY, actualY, tolerance);
        } else if (transformType == TransformationType.ROTATE_OP) {
            // Control if the ROI has been expanded
            if (!isBinary && roiPresent) {
                int imgWidthROI = destinationIMG.getMinX() + destinationIMG.getWidth() / 4 + 1;
                int imgHeightROI = destinationIMG.getMinY() + destinationIMG.getHeight() * 3 / 4 - 1;

                int tileInROIx = planarIMG.XToTileX(imgWidthROI);
                int tileInROIy = planarIMG.YToTileY(imgHeightROI);

                Raster testTile = destinationIMG.getTile(tileInROIx, tileInROIy);

                boolean interpNear = false;
                if (interpN != null) {
                    interpNear = true;
                }

                testROI(dataType, testTile, interpNear);

                // Check minimum and maximum value for a tile
                int xFirstTile = destinationIMG.getMinTileX() + 1; // + destinationIMG.getNumXTiles() - 1;
                int ySecondTile = destinationIMG.getMinTileY() + 1;

                Raster simpleTile = destinationIMG.getTile(xFirstTile, ySecondTile);

                testEmptyImage(dataType, simpleTile, isBinary, interpNear);
            }
            // width
            assertEquals((int) (DEFAULT_WIDTH), destinationIMG.getHeight());
            // height
            assertEquals((int) (DEFAULT_HEIGHT), destinationIMG.getWidth());
        }

        // Final Image disposal
        if (destinationIMG instanceof RenderedOp) {
            ((RenderedOp) destinationIMG).dispose();
        }
    }

    // Test for checking if the ROI is correctly expanded or reduced
    protected void testROI(int dataType, Raster testTile, boolean interpNearest) {
        switch (dataType) {
            case DataBuffer.TYPE_BYTE:
            case DataBuffer.TYPE_USHORT:
            case DataBuffer.TYPE_SHORT:
            case DataBuffer.TYPE_INT:
                int value = 0;
                if (interpNearest) {
                    value = testTile.getSample(testTile.getMinX(), testTile.getMinY() + 2, 0);
                    assertFalse(value == (int) destinationNoData);
                } else {
                    value = testTile.getSample(testTile.getMinX() + 1, testTile.getMinY(), 0);
                    assertFalse(value == (int) destinationNoData);
                }
                break;
            case DataBuffer.TYPE_FLOAT:
                if (interpNearest) {
                    float valuef = testTile.getSampleFloat(testTile.getMinX(), testTile.getMinY() + 2, 0);
                    assertFalse((int) valuef == (int) destinationNoData);
                } else {
                    float valuef = testTile.getSampleFloat(testTile.getMinX(), testTile.getMinY(), 0);
                    assertFalse((int) valuef == (int) destinationNoData);
                }

                break;
            case DataBuffer.TYPE_DOUBLE:
                if (interpNearest) {
                    double valued = testTile.getSampleDouble(testTile.getMinX(), testTile.getMinY() + 2, 0);
                    assertFalse(valued == destinationNoData);
                } else {
                    double valued = testTile.getSampleDouble(testTile.getMinX(), testTile.getMinY(), 0);
                    assertFalse(valued == destinationNoData);
                }
                break;
            default:
                throw new IllegalArgumentException("Wrong data type");
        }
    }

    protected void testEmptyImage(int dataType, Raster simpleTile, boolean isBinary, boolean interpNear) {

        int padding = (interpNear ? 0 : 1);
        int tileminX = simpleTile.getMinX() + padding;
        int tileminY = simpleTile.getMinY() + padding;
        int tileWidth = tileminX + simpleTile.getWidth() - padding;
        int tileHeight = tileminY + simpleTile.getHeight() - padding;

        switch (dataType) {
            case DataBuffer.TYPE_BYTE:
            case DataBuffer.TYPE_USHORT:
            case DataBuffer.TYPE_SHORT:
            case DataBuffer.TYPE_INT:
                int minValue = Integer.MAX_VALUE;
                int maxValue = Integer.MIN_VALUE;

                for (int i = tileminY; i < tileHeight; i++) {
                    for (int j = tileminX; j < tileWidth; j++) {
                        int value = simpleTile.getSample(j, i, 0);
                        if (value > maxValue) {
                            maxValue = value;
                        }

                        if (value < minValue) {
                            minValue = value;
                        }
                    }
                }
                // Check if the values are not max and minimum value
                // assertFalse(minValue == maxValue);
                assertFalse(minValue == Integer.MAX_VALUE);
                assertFalse(maxValue == Integer.MIN_VALUE);
                assertEquals(minValue, destinationNoData, 1E-6);
                assertEquals(maxValue, destinationNoData, 1E-6);
                break;
            case DataBuffer.TYPE_FLOAT:
                float minValuef = Float.MAX_VALUE;
                float maxValuef = -Float.MAX_VALUE;

                for (int i = tileminY; i < tileHeight; i++) {
                    for (int j = tileminX; j < tileWidth; j++) {
                        float valuef = simpleTile.getSample(j, i, 0);
                        if (Float.isNaN(valuef)
                                || valuef == Float.POSITIVE_INFINITY
                                || valuef == Float.POSITIVE_INFINITY) {
                            valuef = 255;
                        }

                        if (valuef > maxValuef) {
                            maxValuef = valuef;
                        }

                        if (valuef < minValuef) {
                            minValuef = valuef;
                        }
                    }
                }
                // Check if the values are not max and minimum value
                // assertFalse((int) minValuef == (int) maxValuef);
                assertFalse(minValuef == Float.MAX_VALUE);
                assertFalse(maxValuef == -Float.MAX_VALUE);
                assertEquals(minValuef, destinationNoData, 1E-6);
                assertEquals(maxValuef, destinationNoData, 1E-6);
                break;
            case DataBuffer.TYPE_DOUBLE:
                double minValued = Double.MAX_VALUE;
                double maxValued = -Double.MAX_VALUE;

                for (int i = tileminY; i < tileHeight; i++) {
                    for (int j = tileminX; j < tileWidth; j++) {
                        double valued = simpleTile.getSampleDouble(j, i, 0);
                        if (Double.isNaN(valued)
                                || valued == Double.POSITIVE_INFINITY
                                || valued == Double.POSITIVE_INFINITY) {
                            valued = 255;
                        }

                        if (valued > maxValued) {
                            maxValued = valued;
                        }

                        if (valued < minValued) {
                            minValued = valued;
                        }
                    }
                }
                // Check if the values are not max and minimum value
                // assertFalse((int) minValued == (int) maxValued);
                assertFalse(minValued == Double.MAX_VALUE);
                assertFalse(maxValued == -Double.MAX_VALUE);
                assertEquals(minValued, destinationNoData, 1E-6);
                assertEquals(maxValued, destinationNoData, 1E-6);
                break;
            default:
                throw new IllegalArgumentException("Wrong data type");
        }
    }

    protected void testGlobalAffine(
            boolean useROIAccessor,
            boolean isBinary,
            boolean bicubic2Disabled,
            boolean noDataRangeUsed,
            boolean roiPresent,
            boolean setDestinationNoData,
            InterpolationType interpType,
            TestSelection testSelect,
            ScaleType scaleValue) {

        Byte sourceNoDataByte = 100;
        Short sourceNoDataUshort = Short.MAX_VALUE - 1;
        Short sourceNoDataShort = -255;
        Integer sourceNoDataInt = Integer.MAX_VALUE - 1;
        Float sourceNoDataFloat = -15.2f;
        Double sourceNoDataDouble = Double.POSITIVE_INFINITY;

        if (isBinary) {
            sourceNoDataByte = 1;
            sourceNoDataUshort = 1;
            sourceNoDataInt = 1;
            // destination no data Value
            destinationNoData = 0;
        } else {
            // destination no data Value
            destinationNoData = 255;
        }

        // ImageTest
        // starting dataType
        int dataType = DataBuffer.TYPE_BYTE;
        testAllOperation(
                dataType,
                isBinary,
                sourceNoDataByte,
                useROIAccessor,
                bicubic2Disabled,
                noDataRangeUsed,
                roiPresent,
                setDestinationNoData,
                interpType,
                testSelect,
                scaleValue);

        dataType = DataBuffer.TYPE_USHORT;
        testAllOperation(
                dataType,
                isBinary,
                sourceNoDataUshort,
                useROIAccessor,
                bicubic2Disabled,
                noDataRangeUsed,
                roiPresent,
                setDestinationNoData,
                interpType,
                testSelect,
                scaleValue);

        dataType = DataBuffer.TYPE_INT;
        testAllOperation(
                dataType,
                isBinary,
                sourceNoDataInt,
                useROIAccessor,
                bicubic2Disabled,
                noDataRangeUsed,
                roiPresent,
                setDestinationNoData,
                interpType,
                testSelect,
                scaleValue);

        if (!isBinary) {
            dataType = DataBuffer.TYPE_SHORT;
            testAllOperation(
                    dataType,
                    isBinary,
                    sourceNoDataShort,
                    useROIAccessor,
                    bicubic2Disabled,
                    noDataRangeUsed,
                    roiPresent,
                    setDestinationNoData,
                    interpType,
                    testSelect,
                    scaleValue);

            dataType = DataBuffer.TYPE_FLOAT;
            testAllOperation(
                    dataType,
                    isBinary,
                    sourceNoDataFloat,
                    useROIAccessor,
                    bicubic2Disabled,
                    noDataRangeUsed,
                    roiPresent,
                    setDestinationNoData,
                    interpType,
                    testSelect,
                    scaleValue);
            dataType = DataBuffer.TYPE_DOUBLE;
            testAllOperation(
                    dataType,
                    isBinary,
                    sourceNoDataDouble,
                    useROIAccessor,
                    bicubic2Disabled,
                    noDataRangeUsed,
                    roiPresent,
                    setDestinationNoData,
                    interpType,
                    testSelect,
                    scaleValue);
        }
    }

    protected <T extends Number & Comparable<? super T>> void testAllOperation(
            int dataType,
            boolean isBinary,
            T sourceNoData,
            boolean useROIAccessor,
            boolean bicubic2Disabled,
            boolean noDataRangeUsed,
            boolean roiPresent,
            boolean setDestinationNoData,
            InterpolationType interpType,
            TestSelection testSelect,
            ScaleType scaleValue) {

        RenderedImage sourceImage = createTestImage(dataType, DEFAULT_WIDTH, DEFAULT_HEIGHT, sourceNoData, isBinary);
        testImageAffine(
                sourceImage,
                dataType,
                sourceNoData,
                useROIAccessor,
                isBinary,
                bicubic2Disabled,
                noDataRangeUsed,
                roiPresent,
                setDestinationNoData,
                TransformationType.ROTATE_OP,
                interpType,
                testSelect,
                scaleValue);
        testImageAffine(
                sourceImage,
                dataType,
                sourceNoData,
                useROIAccessor,
                isBinary,
                bicubic2Disabled,
                noDataRangeUsed,
                roiPresent,
                setDestinationNoData,
                TransformationType.TRANSLATE_OP,
                interpType,
                testSelect,
                scaleValue);
        testImageAffine(
                sourceImage,
                dataType,
                sourceNoData,
                useROIAccessor,
                isBinary,
                bicubic2Disabled,
                noDataRangeUsed,
                roiPresent,
                setDestinationNoData,
                TransformationType.SCALE_OP,
                interpType,
                testSelect,
                scaleValue);
        testImageAffine(
                sourceImage,
                dataType,
                sourceNoData,
                useROIAccessor,
                isBinary,
                bicubic2Disabled,
                noDataRangeUsed,
                roiPresent,
                setDestinationNoData,
                TransformationType.ALL,
                interpType,
                testSelect,
                scaleValue);
    }

    protected void testROILayout(int interpolation) {
        testROILayout(DataBuffer.TYPE_BYTE, interpolation);
        testROILayout(DataBuffer.TYPE_USHORT, interpolation);
        testROILayout(DataBuffer.TYPE_SHORT, interpolation);
        testROILayout(DataBuffer.TYPE_INT, interpolation);
        testROILayout(DataBuffer.TYPE_FLOAT, interpolation);
        testROILayout(DataBuffer.TYPE_DOUBLE, interpolation);
    }

    protected void testROILayout(int dataType, int interpolationType) {
        RenderedImage testIMG = createTestImage(dataType, 1, 1, null, false);
        PlanarImage testImgWithROI = PlanarImage.wrapRenderedImage(testIMG);
        ROI roi = new ROI(new ROIShape(new Rectangle(0, 0, 1, 1)).getAsImage());
        testImgWithROI.setProperty("roi", roi);

        ImageLayout targetLayout = new ImageLayout();
        targetLayout.setTileWidth(512);
        targetLayout.setTileHeight(512);
        RenderingHints hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, targetLayout);
        RenderedOp rotated = AffineDescriptor.create(
                testIMG,
                new AffineTransform(1, -1, 1, 1, 0, 0),
                Interpolation.getInstance(interpolationType),
                null,
                roi,
                false,
                false,
                null,
                hints);
        ROI rotatedRoi = (ROI) rotated.getProperty("roi");

        // ROI is aligned withe the image and has the expected tile size
        assertEquals(rotated.getBounds(), rotatedRoi.getBounds());
        PlanarImage scaleRoiImage = rotatedRoi.getAsImage();
        assertEquals(rotated.getTileHeight(), scaleRoiImage.getTileHeight());
        assertEquals(rotated.getTileWidth(), scaleRoiImage.getTileWidth());
        assertEquals(512, scaleRoiImage.getTileWidth());
        assertEquals(512, scaleRoiImage.getTileHeight());
    }
}
